Skip to content

fix(asr): StepFun 实时转写不再随机白等 12 秒(13% 的听写中招) - #870

Merged
H-Chris233 merged 2 commits into
Open-Less:betafrom
bigsongeth:fix/stepfun-finish-grace-stall
Aug 1, 2026
Merged

fix(asr): StepFun 实时转写不再随机白等 12 秒(13% 的听写中招)#870
H-Chris233 merged 2 commits into
Open-Less:betafrom
bigsongeth:fix/stepfun-finish-grace-stall

Conversation

@bigsongeth

@bigsongeth bigsongeth commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

User description

现象

用 StepFun 实时转写时,随机有一部分听写在松手后要干等 12 秒才出结果。文字是对的,也不报错,日志里一条痕迹都没有。

数据

真机日志 1059 次听写,统计「松手(cpal Stream dropped)→ 出结果(polish dispatch)」耗时:

0s: 655   1s: 227   2s: 43   3s: 11   4s: 3   5s: 1   6s: 1   7s: 1
8~11s: 0
12s: 117   ← 全部落在 12.00~12.97s

中位数 0.87s,8~12 秒之间一次都没有 —— 典型的硬超时兜底,不是网络抖动或模型慢。

按 provider 拆分,只有 StepFun 中招:

provider 总次数 12s 卡顿
stepfun 887 117 (13.2%)
bailian 143 0
bailian-qwen3-realtime 19 0
siliconflow 10 0

原因是其他三家都有协议级 finish 信号(Bailian 的 finish-task、Qwen 的 Finish 事件、Volcengine 的 NegativeSequence 末帧),服务端会回一个确定的终态;StepFun 没有(模块注释已记录:session.finish 被拒、server_vad 下 commit 被静默忽略),只能靠客户端补静音逼 VAD 关段 + 自建状态机判定收尾 —— 出问题的正是这套判定。

影响面不止听写:send_last_frame 被听写、语音问答、重新转录三条链路共用。

根因

两处叠加:

1. contains_non_silent_pcm 用「任意字节 != 0」判静音。 麦克风底噪(实测 RMS≈0.0008)每个采样都非零,于是任何一帧都算「有真实语音」。尾帧因此把 last_non_silent_audio_written_at 刷到最后一个 completed 之后,has_audio_after_last_completed 从此恒为真。

2. 宽限任务只在 FINISH_GRACE 到点查一次。 这一次因上述原因不通过,就再没有第二次机会。而尾帧写出后客户端不再发音频、服务端也不会再吐事件(松手前停顿 ≥VAD 阈值时,最后一段的 completed 早在尾帧之前就到了),于是一路干等到 FINAL_RESULT_TIMEOUT。12s 后走 finish_with_partial_or_error,带着已有文本成功返回 —— 所以文字对、无报错、无日志。

行为学佐证,都指向「说完停顿一下再松手」这个触发条件:

  • 卡顿会话录音末段峰值 RMS < 0.005(已静音)的占 52%,正常会话仅 16%
  • 卡顿会话原始转写 66% 以「嗯。」结尾,正常会话仅 8%

改动

  • contains_non_silent_pcm 改成按振幅判(> 1% 满量程,且至少 8 个采样超阈值以避开键盘/电流的单点尖峰);并且只对尾帧的真实录音部分求值,补的 700ms 静音不参与判定。
  • 宽限任务改成有界轮询(100ms 一次)。收尾前提保持不变 —— 「无未关句段 + 最后一个 completed 之后没有未确认的真实音频」那条保护是对的,不为修卡顿而丢掉;新增 FINISH_HARD_DEADLINE = 3s 兜住服务端始终不关最后一个句段的情况,把最坏等待从 12s 压到 3s。
  • 12s 兜底分支补 log::warn!。这种事不该在日志里完全隐身。

纯客户端时序改动,发给 StepFun 的字节流完全不变,无接口兼容风险。

测试

新增 3 条,修复前全部失败(其中两条重现 12.00s 卡死):

  • silence_detection_ignores_room_tone_but_catches_speech — 底噪/语音/纯静音/单点尖峰四种输入
  • finishes_promptly_when_last_completed_arrived_before_the_tail — 本 issue 的主路径;测试内显式等到 last_completed_at 落地才写尾帧,钉死「completed 先于尾帧」这个时序,否则会走健康快路径复现不出来
  • hard_deadline_bounds_the_wait_when_segment_never_closes — 服务端只发 speech_started + delta、永不 completed,验证 3s 硬上限生效且未 completed 的 interim 尾巴仍被带出、不丢字

既有的 non_silent_audio_waits_for_delayed_vad_before_finishing 保持通过(它保证的「有未确认语音时不能提前收尾」是被有意保留的),只换了夹具:[1u8; N] 在新判据下属于底噪,改用语音级振幅。

cargo check --tests 通过;cargo test --lib asr::stepfun_realtime 19/19 通过。

🤖 Generated with Claude Code


PR Type

Bug fix, Tests


Description

  • 修复 StepFun 实时转写约 13% 听写随机白等 12 秒的问题

  • 根因:contains_non_silent_pcm 把底噪当语音,导致宽限检查一次不通过且无重试

  • 修复:按振幅判静音、宽限期轮询复查,并增加 3 秒硬上限兜底

  • 新增回归测试覆盖“completed 早于尾帧”和“句段永不关闭”两种场景


Diagram Walkthrough

flowchart LR
  A["尾帧写出"] --> B["每100ms轮询收尾条件"]
  B -->|"宽限内无未关句段且无新语音"| C["finish_success"]
  B -->|"超过3秒硬上限"| D{"有可返回文本?"}
  D -->|"是"| C
  D -->|"否"| E["等待至 FINAL_RESULT_TIMEOUT 后显式报错"]
Loading

File Walkthrough

Relevant files
Bug fix
stepfun_realtime.rs
修复 StepFun 收尾静音误判与 12 秒超时卡顿                                                           

openless-all/app/src-tauri/src/asr/stepfun_realtime.rs

  • contains_non_silent_pcm 从“任一字节非零”改为按振幅阈值与最少采样数判断,避免底噪误判
  • 尾帧收尾逻辑由单次宽限检查改为 FINISH_POLL_INTERVAL 轮询,并增加 FINISH_HARD_DEADLINE(3
    秒)强制收尾兜底
  • 提取 has_transcriptshould_force_finish_at_hard_deadline,统一部分结果返回与硬截止行为
  • 新增 4 个测试:底噪/语音识别、硬截止状态机、两类真实收尾场景回归测试
+274/-16

真机日志(1059 次听写)统计「松手 → 出结果」耗时:中位数 0.87s,但有 117 次
精确落在 12.00~12.97s,8~12s 之间一次都没有 —— 是硬超时兜底,不是网络抖动。
按 provider 拆分后只有 StepFun 中招:117/887 = 13.2%,bailian / qwen3-realtime
/ siliconflow 合计 0/172。三条语音链路(听写、语音问答、重新转录)共用同一个
send_last_frame,全都受影响,且全程不打任何日志。

根因在收尾状态机:

1. contains_non_silent_pcm 用「任意字节 != 0」判静音。麦克风底噪(实测
   RMS≈0.0008)每个采样都非零,于是尾帧永远被判成「有真实语音」,把
   last_non_silent_audio_written_at 刷到最后一个 completed 之后 ——
   has_audio_after_last_completed 从此恒为真。
2. 宽限任务只在 FINISH_GRACE 到点查一次。这一次因上述原因不通过,就再没有第二
   次机会;而尾帧写出后客户端不再发音频,服务端也不会再吐事件(松手前停顿
   ≥VAD 阈值时,最后一段的 completed 早在尾帧之前就到了),于是一路干等到
   FINAL_RESULT_TIMEOUT。12s 后带着 partial 成功返回,所以文字是对的、也没报错。

对应三处修改:

- contains_non_silent_pcm 改成按振幅判(>1% 满量程、且至少 8 个采样,避开单点
  尖峰),并且只对尾帧的真实录音部分求值,补的静音不参与。
- 宽限任务改成有界轮询(100ms 一次)。收尾前提保持不变 —— 「无未关句段 + 最后
  一个 completed 之后没有未确认的真实音频」那条保护是对的,不为修卡顿而丢;
  新增 FINISH_HARD_DEADLINE=3s 兜住服务端始终不关句段的情况。
- 12s 兜底分支补 log::warn!,这种事不该在日志里完全隐身。

行为学佐证:卡顿会话录音末段峰值 RMS<0.005(已静音)的占 52%,正常会话仅 16%;
卡顿会话原始转写 66% 以「嗯。」结尾,正常会话仅 8% —— 都指向「说完停顿一下再
松手」这个触发条件。

测试:三条新增测试在修复前均失败(两条重现 12.00s 卡死),修复后 19/19 通过。
既有的 non_silent_audio_waits_for_delayed_vad_before_finishing 保持通过,只换了
夹具([1u8; N] 在新判据下属于底噪,改用语音级振幅)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 491be37)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@H-Chris233 H-Chris233 self-assigned this Aug 1, 2026
@H-Chris233

Copy link
Copy Markdown
Collaborator

大松你真牛逼,测试1059次,太牛逼了

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 491be37

@H-Chris233
H-Chris233 merged commit 1ab2d52 into Open-Less:beta Aug 1, 2026
5 checks passed
@bigsongeth

bigsongeth commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

大松你真牛逼,测试1059次,太牛逼了

项目的重度用户啊,每天我至少要用 openless 说一个小时的话

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants